package tcln.sors.service.core.utils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.apache.commons.collections.CollectionUtils;
import org.dozer.DozerBeanMapper;
import org.dozer.Mapper;

import tcln.commons.assertion.Assert;

/**
 * @param <S>
 *            Source class.
 * @param <D>
 *            Destination class.
 * @author Truong Chau Lien Ngoc (lienngoc@gmail.com)
 */
public class BeanDuplicator<S, D> {
    private Class<S> sourceClass;
    private Class<D> destinationClass;
    private Mapper mapper;

    /**
     * Creates a new {@link BeanDuplicator} for given source/dest classes and an optional {@link Mapper} implementation.
     * 
     * @param <S>
     * @param <D>
     * @param sourceClass
     * @param destClass
     * @param mapper
     * @return
     */
    public static <S, D> BeanDuplicator<S, D> createFor(Class<S> sourceClass, Class<D> destClass, Mapper mapper) {
        return new BeanDuplicator<S, D>(sourceClass, destClass, mapper);
    }

    /**
     * Creates a new {@link BeanDuplicator} for given source/dest classes and a default {@link Mapper} implementation.
     * 
     * @param <S>
     * @param <D>
     * @param sourceClass
     * @param destClass
     * @return
     */
    public static <S, D> BeanDuplicator<S, D> createFor(Class<S> sourceClass, Class<D> destClass) {
        return new BeanDuplicator<S, D>(sourceClass, destClass, null);
    }

    private BeanDuplicator(Class<S> sourceClass, Class<D> destClass, Mapper mapper) {
        this.mapper = mapper != null ? mapper : new DozerBeanMapper();
        this.sourceClass = sourceClass;
        this.destinationClass = destClass;
    }

    /**
     * Duplicates a given source object.
     * 
     * @param source
     *            Source object. <code>null</code> will produce a <code>null</code> result.
     * @return
     */
    public D duplicate(S source) {
        if (source == null) {
            return null;
        }
        
        ensureRequirementsBeforeDuplication();
        Assert.ARGUMENTS.isTrue(sourceClass.isAssignableFrom(source.getClass()), "Source object is not a {0}",
                sourceClass);
        
        return mapper.map(source, destinationClass);
    }

    /**
     * Duplicates a given source object.
     * 
     * @param source
     *            Source object. <code>null</code> will produce a <code>null</code> result.
     * @param dest
     *            Destination object which contains data to be copied.
     * @return The destinationi object after copying data from the source one.
     */
    public D duplicate(S source, D dest) {
        if (source == null) {
            return null;
        }
        
        ensureRequirementsBeforeDuplication();
        Assert.ARGUMENTS.isTrue(sourceClass.isAssignableFrom(source.getClass()), "Source object is not a {0}",
                sourceClass);
        Assert.ARGUMENTS.isTrue(destinationClass.isAssignableFrom(dest.getClass()), "Destination object is not a {0}",
                destinationClass);
        
        mapper.map(source, dest);
        
        return dest;
    }

    /**
     * Duplicates all given source collection to a new collection.
     * 
     * @param sources
     * @return
     */
    public List<D> duplicateAll(Collection<S> sources) {
        if (CollectionUtils.isEmpty(sources)) {
            return Collections.emptyList();
        }

        List<D> results = new ArrayList<D>(sources.size());
        for (S src : sources) {
            D dest = duplicate(src);
            results.add(dest);
        } // End: (S src : sources)

        return results;
    }
    
    protected void ensureRequirementsBeforeDuplication() {
        Assert.STATE.notNull(sourceClass, "Source class must not be null");
        Assert.STATE.notNull(destinationClass, "Destination class must not be null");
    }
}
